音视频数据处理入门

音视频数据处理入门

有段时间没有写博客了,这两天写起博客来竟然感觉有些兴奋,仿佛找回了原来的感觉。前一阵子在梳理以前文章的时候,发现自己虽然总结了各种视音频应用程序,却还缺少一个适合无视音频背景人员学习的 “最基础” 的程序。因此抽时间将以前写过的代码整理成了一个小项目。这个小项目里面包含了一系列简单的函数,可以对 RGB/YUV 视频像素数据、PCM 音频采样数据、H.264 视频码流、AAC 音频码流、FLV 封装格式数据、UDP/RTP 协议数据进行简单处理。这个项目的一大特点就是没有使用任何的第三方类库,完全借助于 C 语言的基本函数实现了功能。通过对这些代码的学习,可以让初学者迅速掌握视音频数据的基本格式。有关上述几种格式的介绍可以参考文章《[总结] 视音频编解码技术零基础学习方法》。

从这篇文章开始打算写 6 篇文章分别记录上述 6 种不同类型的视音频数据的处理方法。本文首先记录第一部分即 RGB/YUV 视频像素数据的处理方法。视频像素数据在视频播放器的解码流程中的位置如下图所示。

RGB、YUV 像素数据处理

本文分别介绍如下几个 RGB/YUV 视频像素数据处理函数: 分离 YUV420P 像素数据中的 Y、U、V 分量 分离 YUV444P 像素数据中的 Y、U、V 分量 将 YUV420P 像素数据去掉颜色(变成灰度图) 将 YUV420P 像素数据的亮度减半 将 YUV420P 像素数据的周围加上边框 生成 YUV420P 格式的灰阶测试图 计算两个 YUV420P 像素数据的 PSNR 分离 RGB24 像素数据中的 R、G、B 分量 将 RGB24 格式像素数据封装为 BMP 图像 将 RGB24 格式像素数据转换为 YUV420P 格式像素数据 生成 RGB24 格式的彩条测试图

本文中的 RGB/YUV 文件需要使用 RGB/YUV 播放器才能查看。YUV 播放器种类比较多,例如 YUV Player Deluxe,或者开源播放器(参考文章《修改了一个 YUV/RGB 播放器》)等。

函数列表

(1) 分离 YUV420P 像素数据中的 Y、U、V 分量

本程序中的函数可以将 YUV420P 数据中的 Y、U、V 三个分量分离开来并保存成三个文件。函数的代码如下所示。

/**
 * Split Y, U, V planes in YUV420P file.
 * @param url  Location of Input YUV file.
 * @param w    Width of Input YUV file.
 * @param h    Height of Input YUV file.
 * @param num  Number of frames to process.
 *
 */
int simplest_yuv420_split(char *url, int w, int h,int num){
	FILE *fp=fopen(url,"rb+");
	FILE *fp1=fopen("output_420_y.y","wb+");
	FILE *fp2=fopen("output_420_u.y","wb+");
	FILE *fp3=fopen("output_420_v.y","wb+");
  
	unsigned char *pic=(unsigned char *)malloc(w*h*3/2);
  
	for(int i=0;i<num;i++){
  
		fread(pic,1,w*h*3/2,fp);
		//Y
		fwrite(pic,1,w*h,fp1);
		//U
		fwrite(pic+w*h,1,w*h/4,fp2);
		//V
		fwrite(pic+w*h*5/4,1,w*h/4,fp3);
	}
  
	free(pic);
	fclose(fp);
	fclose(fp1);
	fclose(fp2);
	fclose(fp3);
  
	return 0;
}

调用上面函数的方法如下所示。

simplest_yuv420_split("lena_256x256_yuv420p.yuv",256,256,1);

从代码可以看出,如果视频帧的宽和高分别为 w 和 h,那么一帧 YUV420P 像素数据一共占用 wh3/2 Byte 的数据。其中前 wh Byte 存储 Y,接着的 wh1/4 Byte 存储 U,最后 wh*1/4 Byte 存储 V。上述调用函数的代码运行后,将会把一张分辨率为 256x256 的名称为 lena_256x256_yuv420p.yuv 的 YUV420P 格式的像素数据文件分离成为三个文件:

output_420_y.y:纯 Y 数据,分辨率为 256x256。

output_420_u.y:纯 U 数据,分辨率为 128x128。

output_420_v.y:纯 V 数据,分辨率为 128x128。

注:本文中像素的采样位数一律为 8bit。由于 1Byte=8bit,所以一个像素的一个分量的采样值占用 1Byte。

程序输入的原图如下所示。

lena_256x256_yuv420p.yuv

程序输出的三个文件的截图如下图所示。在这里需要注意输出的 U、V 分量在 YUV 播放器中也是当做 Y 分量进行播放的。

 

output_420_y.y

           

output_420_u.y 和 output_420_v.y

(2) 分离 YUV444P 像素数据中的 Y、U、V 分量

本程序中的函数可以将 YUV444P 数据中的 Y、U、V 三个分量分离开来并保存成三个文件。函数的代码如下所示。

/**
 * Split Y, U, V planes in YUV444P file.
 * @param url  Location of YUV file.
 * @param w    Width of Input YUV file.
 * @param h    Height of Input YUV file.
 * @param num  Number of frames to process.
 *
 */
int simplest_yuv444_split(char *url, int w, int h,int num){
	FILE *fp=fopen(url,"rb+");
	FILE *fp1=fopen("output_444_y.y","wb+");
	FILE *fp2=fopen("output_444_u.y","wb+");
	FILE *fp3=fopen("output_444_v.y","wb+");
	unsigned char *pic=(unsigned char *)malloc(w*h*3);
  
	for(int i=0;i<num;i++){
		fread(pic,1,w*h*3,fp);
		//Y
		fwrite(pic,1,w*h,fp1);
		//U
		fwrite(pic+w*h,1,w*h,fp2);
		//V
		fwrite(pic+w*h*2,1,w*h,fp3);
	}
  
	free(pic);
	fclose(fp);
	fclose(fp1);
	fclose(fp2);
	fclose(fp3);
  
	return 0;
}

调用上面函数的方法如下所示。

simplest_yuv444_split("lena_256x256_yuv444p.yuv",256,256,1);

从代码可以看出,如果视频帧的宽和高分别为 w 和 h,那么一帧 YUV444P 像素数据一共占用 wh3 Byte 的数据。其中前 wh Byte 存储 Y,接着的 wh Byte 存储 U,最后 w*h Byte 存储 V。上述调用函数的代码运行后,将会把一张分辨率为 256x256 的名称为 lena_256x256_yuv444p.yuv 的 YUV444P 格式的像素数据文件分离成为三个文件:

output_444_y.y:纯 Y 数据,分辨率为 256x256。 output_444_u.y:纯 U 数据,分辨率为 256x256。 output_444_v.y:纯 V 数据,分辨率为 256x256。

输入的原图如下所示。

输出的三个文件的截图如下图所示。

 

output_444_y.y

 

output_444_u.y

 

output_444_v.y

(3) 将 YUV420P 像素数据去掉颜色(变成灰度图)

本程序中的函数可以将 YUV420P 格式像素数据的彩色去掉,变成纯粹的灰度图。函数的代码如下。

/**
 * Convert YUV420P file to gray picture
 * @param url     Location of Input YUV file.
 * @param w       Width of Input YUV file.
 * @param h       Height of Input YUV file.
 * @param num     Number of frames to process.
 */
int simplest_yuv420_gray(char *url, int w, int h,int num){
	FILE *fp=fopen(url,"rb+");
	FILE *fp1=fopen("output_gray.yuv","wb+");
	unsigned char *pic=(unsigned char *)malloc(w*h*3/2);
  
	for(int i=0;i<num;i++){
		fread(pic,1,w*h*3/2,fp);
		//Gray
		memset(pic+w*h,128,w*h/2);
		fwrite(pic,1,w*h*3/2,fp1);
	}
  
	free(pic);
	fclose(fp);
	fclose(fp1);
	return 0;
}
```
  
```cpp
simplest_yuv420_gray("lena_256x256_yuv420p.yuv",256,256,1);

从代码可以看出,如果想把 YUV 格式像素数据变成灰度图像,只需要将 U、V 分量设置成 128 即可。这是因为 U、V 是图像中的经过偏置处理的色度分量。色度分量在偏置处理前的取值范围是 - 128 至 127,这时候的无色对应的是 “0” 值。经过偏置后色度分量取值变成了 0 至 255,因而此时的无色对应的就是 128 了。上述调用函数的代码运行后,将会把一张分辨率为 256x256 的名称为 lena_256x256_yuv420p.yuv 的 YUV420P 格式的像素数据文件处理成名称为 output_gray.yuv 的 YUV420P 格式的像素数据文件。输入的原图如下所示。  处理后的图像如下所示。  

(4) 将 YUV420P 像素数据的亮度减半

本程序中的函数可以通过将 YUV 数据中的亮度分量 Y 的数值减半的方法,降低图像的亮度。函数代码如下所示。

/**
 * Halve Y value of YUV420P file
 * @param url     Location of Input YUV file.
 * @param w       Width of Input YUV file.
 * @param h       Height of Input YUV file.
 * @param num     Number of frames to process.
 */
int simplest_yuv420_halfy(char *url, int w, int h,int num){
	FILE *fp=fopen(url,"rb+");
	FILE *fp1=fopen("output_half.yuv","wb+");
  
	unsigned char *pic=(unsigned char *)malloc(w*h*3/2);
  
	for(int i=0;i<num;i++){
		fread(pic,1,w*h*3/2,fp);
		//Half
		for(int j=0;j<w*h;j++){
			unsigned char temp=pic[j]/2;
			//printf("%d,\n",temp);
			pic[j]=temp;
		}
		fwrite(pic,1,w*h*3/2,fp1);
	}
  
	free(pic);
	fclose(fp);
	fclose(fp1);
  
	return 0;
}

调用上面函数的方法如下所示。

simplest_yuv420_halfy("lena_256x256_yuv420p.yuv",256,256,1);

从代码可以看出,如果打算将图像的亮度减半,只要将图像的每个像素的 Y 值取出来分别进行除以 2 的工作就可以了。图像的每个 Y 值占用 1 Byte,取值范围是 0 至 255,对应 C 语言中的 unsigned char 数据类型。上述调用函数的代码运行后,将会把一张分辨率为 256x256 的名称为 lena_256x256_yuv420p.yuv 的 YUV420P 格式的像素数据文件处理成名称为 output_half.yuv 的 YUV420P 格式的像素数据文件。输入的原图如下所示。

处理后的图像如下所示。  

(5) 将 YUV420P 像素数据的周围加上边框

本程序中的函数可以通过修改 YUV 数据中特定位置的亮度分量 Y 的数值,给图像添加一个 “边框” 的效果。函数代码如下所示。

/**
 * Add border for YUV420P file
 * @param url     Location of Input YUV file.
 * @param w       Width of Input YUV file.
 * @param h       Height of Input YUV file.
 * @param border  Width of Border.
 * @param num     Number of frames to process.
 */
int simplest_yuv420_border(char *url, int w, int h,int border,int num){
	FILE *fp=fopen(url,"rb+");
	FILE *fp1=fopen("output_border.yuv","wb+");
  
	unsigned char *pic=(unsigned char *)malloc(w*h*3/2);
  
	for(int i=0;i<num;i++){
		fread(pic,1,w*h*3/2,fp);
		//Y
		for(int j=0;j<h;j++){
			for(int k=0;k<w;k++){
				if(k<border||k>(w-border)||j<border||j>(h-border)){
					pic[j*w+k]=255;
					//pic[j*w+k]=0;
				}
			}
		}
		fwrite(pic,1,w*h*3/2,fp1);
	}
  
	free(pic);
	fclose(fp);
	fclose(fp1);
  
	return 0;
}

调用上面函数的方法如下所示。

simplest_yuv420_border("lena_256x256_yuv420p.yuv",256,256,20,1);

从代码可以看出,图像的边框的宽度为 border,本程序将距离图像边缘 border 范围内的像素的亮度分量 Y 的取值设置成了亮度最大值 255。上述调用函数的代码运行后,将会把一张分辨率为 256x256 的名称为 lena_256x256_yuv420p.yuv 的 YUV420P 格式的像素数据文件处理成名称为 output_border.yuv 的 YUV420P 格式的像素数据文件。输入的原图如下所示。

处理后的图像如下所示。

(6) 生成 YUV420P 格式的灰阶测试图

本程序中的函数可以生成一张 YUV420P 格式的灰阶测试图。函数代码如下所示。

/**
 * Generate YUV420P gray scale bar.
 * @param width    Width of Output YUV file.
 * @param height   Height of Output YUV file.
 * @param ymin     Max value of Y
 * @param ymax     Min value of Y
 * @param barnum   Number of bars
 * @param url_out  Location of Output YUV file.
 */
int simplest_yuv420_graybar(int width, int height,int ymin,int ymax,int barnum,char *url_out){
	int barwidth;
	float lum_inc;
	unsigned char lum_temp;
	int uv_width,uv_height;
	FILE *fp=NULL;
	unsigned char *data_y=NULL;
	unsigned char *data_u=NULL;
	unsigned char *data_v=NULL;
	int t=0,i=0,j=0;
  
	barwidth=width/barnum;
	lum_inc=((float)(ymax-ymin))/((float)(barnum-1));
	uv_width=width/2;
	uv_height=height/2;
  
	data_y=(unsigned char *)malloc(width*height);
	data_u=(unsigned char *)malloc(uv_width*uv_height);
	data_v=(unsigned char *)malloc(uv_width*uv_height);
  
	if((fp=fopen(url_out,"wb+"))==NULL){
		printf("Error: Cannot create file!");
		return -1;
	}
  
	//Output Info
	printf("Y, U, V value from picture's left to right:\n");
	for(t=0;t<(width/barwidth);t++){
		lum_temp=ymin+(char)(t*lum_inc);
		printf("%3d, 128, 128\n",lum_temp);
	}
	//Gen Data
	for(j=0;j<height;j++){
		for(i=0;i<width;i++){
			t=i/barwidth;
			lum_temp=ymin+(char)(t*lum_inc);
			data_y[j*width+i]=lum_temp;
		}
	}
	for(j=0;j<uv_height;j++){
		for(i=0;i<uv_width;i++){
			data_u[j*uv_width+i]=128;
		}
	}
	for(j=0;j<uv_height;j++){
		for(i=0;i<uv_width;i++){
			data_v[j*uv_width+i]=128;
		}
	}
	fwrite(data_y,width*height,1,fp);
	fwrite(data_u,uv_width*uv_height,1,fp);
	fwrite(data_v,uv_width*uv_height,1,fp);
	fclose(fp);
	free(data_y);
	free(data_u);
	free(data_v);
	return 0;
}

调用上面函数的方法如下所示。

simplest_yuv420_graybar(640, 360,0,255,10,"graybar_640x360.yuv");

从源代码可以看出,本程序一方面通过灰阶测试图的亮度最小值 ymin,亮度最大值 ymax,灰阶数量 barnum 确定每一个灰度条中像素的亮度分量 Y 的取值。另一方面还要根据图像的宽度 width 和图像的高度 height 以及灰阶数量 barnum 确定每一个灰度条的宽度。有了这两方面信息之后,就可以生成相应的图片了。上述调用函数的代码运行后,会生成一个取值范围从 0-255,一共包含 10 个灰度条的 YUV420P 格式的测试图。测试图的内容如下所示。  从程序也可以得到从左到右 10 个灰度条的 Y、U、V 取值,如下所示。

(7) 计算两个 YUV420P 像素数据的 PSNR

PSNR 是最基本的视频质量评价方法。本程序中的函数可以对比两张 YUV 图片中亮度分量 Y 的 PSNR。函数的代码如下所示。

/**
 * Calculate PSNR between 2 YUV420P file
 * @param url1     Location of first Input YUV file.
 * @param url2     Location of another Input YUV file.
 * @param w        Width of Input YUV file.
 * @param h        Height of Input YUV file.
 * @param num      Number of frames to process.
 */
int simplest_yuv420_psnr(char *url1,char *url2,int w,int h,int num){
	FILE *fp1=fopen(url1,"rb+");
	FILE *fp2=fopen(url2,"rb+");
	unsigned char *pic1=(unsigned char *)malloc(w*h);
	unsigned char *pic2=(unsigned char *)malloc(w*h);
  
	for(int i=0;i<num;i++){
		fread(pic1,1,w*h,fp1);
		fread(pic2,1,w*h,fp2);
  
		double mse_sum=0,mse=0,psnr=0;
		for(int j=0;j<w*h;j++){
			mse_sum+=pow((double)(pic1[j]-pic2[j]),2);
		}
		mse=mse_sum/(w*h);
		psnr=10*log10(255.0*255.0/mse);
		printf("%5.3f\n",psnr);
  
		fseek(fp1,w*h/2,SEEK_CUR);
		fseek(fp2,w*h/2,SEEK_CUR);
  
	}
  
	free(pic1);
	free(pic2);
	fclose(fp1);
	fclose(fp2);
	return 0;
}

调用上面函数的方法如下所示。

simplest_yuv420_psnr("lena_256x256_yuv420p.yuv","lena_distort_256x256_yuv420p.yuv",256,256,1);

对于 8bit 量化的像素数据来说,PSNR 的计算公式如下所示。

上述公式中 mse 的计算公式如下所示。

其中 M,N 分别为图像的宽高,xij 和 yij 分别为两张图像的每一个像素值。PSNR 通常用于质量评价,就是计算受损图像与原始图像之间的差别,以此来评价受损图像的质量。本程序输入的两张图像的对比图如下图所示。其中左边的图像为原始图像,右边的图像为受损图像。

经过程序计算后得到的 PSNR 取值为 26.693。PSNR 取值通常情况下都在 20-50 的范围内,取值越高,代表两张图像越接近,反映出受损图像质量越好。

(8) 分离 RGB24 像素数据中的 R、G、B 分量

本程序中的函数可以将 RGB24 数据中的 R、G、B 三个分量分离开来并保存成三个文件。函数的代码如下所示。

/**
 * Split R, G, B planes in RGB24 file.
 * @param url  Location of Input RGB file.
 * @param w    Width of Input RGB file.
 * @param h    Height of Input RGB file.
 * @param num  Number of frames to process.
 *
 */
int simplest_rgb24_split(char *url, int w, int h,int num){
	FILE *fp=fopen(url,"rb+");
	FILE *fp1=fopen("output_r.y","wb+");
	FILE *fp2=fopen("output_g.y","wb+");
	FILE *fp3=fopen("output_b.y","wb+");
  
	unsigned char *pic=(unsigned char *)malloc(w*h*3);
  
	for(int i=0;i<num;i++){
  
		fread(pic,1,w*h*3,fp);
  
		for(int j=0;j<w*h*3;j=j+3){
			//R
			fwrite(pic+j,1,1,fp1);
			//G
			fwrite(pic+j+1,1,1,fp2);
			//B
			fwrite(pic+j+2,1,1,fp3);
		}
	}
  
	free(pic);
	fclose(fp);
	fclose(fp1);
	fclose(fp2);
	fclose(fp3);
  
	return 0;
}

调用上面函数的方法如下所示。

simplest_rgb24_split("cie1931_500x500.rgb", 500, 500,1);

从代码可以看出,与 YUV420P 三个分量分开存储不同,RGB24 格式的每个像素的三个分量是连续存储的。一帧宽高分别为 w、h 的 RGB24 图像一共占用 wh3 Byte 的存储空间。RGB24 格式规定首先存储第一个像素的 R、G、B,然后存储第二个像素的 R、G、B… 以此类推。类似于 YUV420P 的存储方式称为 Planar 方式,而类似于 RGB24 的存储方式称为 Packed 方式。上述调用函数的代码运行后,将会把一张分辨率为 500x500 的名称为 cie1931_500x500.rgb 的 RGB24 格式的像素数据文件分离成为三个文件:

output_r.y:R 数据,分辨率为 256x256。 output_g.y:G 数据,分辨率为 256x256。

output_b.y:B 数据,分辨率为 256x256。

输入的原图是一张标准的 CIE 1931 色度图。该色度图右下为红色,上方为绿色,左下为蓝色,如下所示。

R 数据图像如下所示。

G 数据图像如下所示。

B 数据图像如下所示。

(9) 将 RGB24 格式像素数据封装为 BMP 图像

BMP 图像内部实际上存储的就是 RGB 数据。本程序实现了对 RGB 像素数据的封装处理。通过本程序中的函数,可以将 RGB 数据封装成为一张 BMP 图像。

/**
 * Convert RGB24 file to BMP file
 * @param rgb24path    Location of input RGB file.
 * @param width        Width of input RGB file.
 * @param height       Height of input RGB file.
 * @param url_out      Location of Output BMP file.
 */
int simplest_rgb24_to_bmp(const char *rgb24path,int width,int height,const char *bmppath){
	typedef struct
	{
		long imageSize;
		long blank;
		long startPosition;
	}BmpHead;
  
	typedef struct
	{
		long  Length;
		long  width;
		long  height;
		unsigned short  colorPlane;
		unsigned short  bitColor;
		long  zipFormat;
		long  realSize;
		long  xPels;
		long  yPels;
		long  colorUse;
		long  colorImportant;
	}InfoHead;
  
	int i=0,j=0;
	BmpHead m_BMPHeader={0};
	InfoHead  m_BMPInfoHeader={0};
	char bfType[2]={'B','M'};
	int header_size=sizeof(bfType)+sizeof(BmpHead)+sizeof(InfoHead);
	unsigned char *rgb24_buffer=NULL;
	FILE *fp_rgb24=NULL,*fp_bmp=NULL;
  
	if((fp_rgb24=fopen(rgb24path,"rb"))==NULL){
		printf("Error: Cannot open input RGB24 file.\n");
		return -1;
	}
	if((fp_bmp=fopen(bmppath,"wb"))==NULL){
		printf("Error: Cannot open output BMP file.\n");
		return -1;
	}
  
	rgb24_buffer=(unsigned char *)malloc(width*height*3);
	fread(rgb24_buffer,1,width*height*3,fp_rgb24);
  
	m_BMPHeader.imageSize=3*width*height+header_size;
	m_BMPHeader.startPosition=header_size;
  
	m_BMPInfoHeader.Length=sizeof(InfoHead);
	m_BMPInfoHeader.width=width;
	//BMP storage pixel data in opposite direction of Y-axis (from bottom to top).
	m_BMPInfoHeader.height=-height;
	m_BMPInfoHeader.colorPlane=1;
	m_BMPInfoHeader.bitColor=24;
	m_BMPInfoHeader.realSize=3*width*height;
  
	fwrite(bfType,1,sizeof(bfType),fp_bmp);
	fwrite(&m_BMPHeader,1,sizeof(m_BMPHeader),fp_bmp);
	fwrite(&m_BMPInfoHeader,1,sizeof(m_BMPInfoHeader),fp_bmp);
  
	//BMP save R1|G1|B1,R2|G2|B2 as B1|G1|R1,B2|G2|R2
	//It saves pixel data in Little Endian
	//So we change 'R' and 'B'
	for(j =0;j<height;j++){
		for(i=0;i<width;i++){
			char temp=rgb24_buffer[(j*width+i)*3+2];
			rgb24_buffer[(j*width+i)*3+2]=rgb24_buffer[(j*width+i)*3+0];
			rgb24_buffer[(j*width+i)*3+0]=temp;
		}
	}
	fwrite(rgb24_buffer,3*width*height,1,fp_bmp);
	fclose(fp_rgb24);
	fclose(fp_bmp);
	free(rgb24_buffer);
	printf("Finish generate %s!\n",bmppath);
	return 0;
	return 0;
}

调用上面函数的方法如下所示。

simplest_rgb24_to_bmp("lena_256x256_rgb24.rgb",256,256,"output_lena.bmp");

通过代码可以看出,改程序完成了主要完成了两个工作:

  1. 将 RGB 数据前面加上文件头。 2)将 RGB 数据中每个像素的 “B” 和“R”的位置互换。

BMP 文件是由 BITMAPFILEHEADER、BITMAPINFOHEADER、RGB 像素数据共 3 个部分构成,它的结构如下图所示。

其中前两部分的结构如下所示。在写入 BMP 文件头的时候给其中的每个字段赋上合适的值就可以了。

typedef  struct  tagBITMAPFILEHEADER
{
unsigned short int  bfType;       //位图文件的类型,必须为BM
unsigned long       bfSize;       //文件大小,以字节为单位
unsigned short int  bfReserverd1; //位图文件保留字,必须为0
unsigned short int  bfReserverd2; //位图文件保留字,必须为0
unsigned long       bfbfOffBits;  //位图文件头到数据的偏移量,以字节为单位
}BITMAPFILEHEADER;
typedef  struct  tagBITMAPINFOHEADER
{
long biSize;                        //该结构大小,字节为单位
long  biWidth;                     //图形宽度以象素为单位
long  biHeight;                     //图形高度以象素为单位
short int  biPlanes;               //目标设备的级别,必须为1
short int  biBitcount;             //颜色深度,每个象素所需要的位数
short int  biCompression;        //位图的压缩类型
long  biSizeImage;              //位图的大小,以字节为单位
long  biXPelsPermeter;       //位图水平分辨率,每米像素数
long  biYPelsPermeter;       //位图垂直分辨率,每米像素数
long  biClrUsed;            //位图实际使用的颜色表中的颜色数
long  biClrImportant;       //位图显示过程中重要的颜色数
}BITMAPINFOHEADER;

BMP 采用的是小端(Little Endian)存储方式。这种存储方式中 “RGB24” 格式的像素的分量存储的先后顺序为 B、G、R。由于 RGB24 格式存储的顺序是 R、G、B,所以需要将 “R” 和“B”顺序作一个调换再进行存储。

下图为输入的 RGB24 格式的图像 lena_256x256_rgb24.rgb。

下图分封装为 BMP 格式后的图像 output_lena.bmp。封装后的图像使用普通的看图软件就可以查看。

(10) 将 RGB24 格式像素数据转换为 YUV420P 格式像素数据

本程序中的函数可以将 RGB24 格式的像素数据转换为 YUV420P 格式的像素数据。函数的代码如下所示。

unsigned char clip_value(unsigned char x,unsigned char min_val,unsigned char  max_val){
	if(x>max_val){
		return max_val;
	}else if(x<min_val){
		return min_val;
	}else{
		return x;
	}
}
  
//RGB to YUV420
bool RGB24_TO_YUV420(unsigned char *RgbBuf,int w,int h,unsigned char *yuvBuf)
{
	unsigned char*ptrY, *ptrU, *ptrV, *ptrRGB;
	memset(yuvBuf,0,w*h*3/2);
	ptrY = yuvBuf;
	ptrU = yuvBuf + w*h;
	ptrV = ptrU + (w*h*1/4);
	unsigned char y, u, v, r, g, b;
	for (int j = 0; j<h;j++){
		ptrRGB = RgbBuf + w*j*3 ;
		for (int i = 0;i<w;i++){
  
			r = *(ptrRGB++);
			g = *(ptrRGB++);
			b = *(ptrRGB++);
			y = (unsigned char)( ( 66 * r + 129 * g +  25 * b + 128) >> 8) + 16  ;
			u = (unsigned char)( ( -38 * r -  74 * g + 112 * b + 128) >> 8) + 128 ;
			v = (unsigned char)( ( 112 * r -  94 * g -  18 * b + 128) >> 8) + 128 ;
			*(ptrY++) = clip_value(y,0,255);
			if (j%2==0&&i%2 ==0){
				*(ptrU++) =clip_value(u,0,255);
			}
			else{
				if (i%2==0){
				*(ptrV++) =clip_value(v,0,255);
				}
			}
		}
	}
	return true;
}
  
/**
 * Convert RGB24 file to YUV420P file
 * @param url_in  Location of Input RGB file.
 * @param w       Width of Input RGB file.
 * @param h       Height of Input RGB file.
 * @param num     Number of frames to process.
 * @param url_out Location of Output YUV file.
 */
int simplest_rgb24_to_yuv420(char *url_in, int w, int h,int num,char *url_out){
	FILE *fp=fopen(url_in,"rb+");
	FILE *fp1=fopen(url_out,"wb+");
  
	unsigned char *pic_rgb24=(unsigned char *)malloc(w*h*3);
	unsigned char *pic_yuv420=(unsigned char *)malloc(w*h*3/2);
  
	for(int i=0;i<num;i++){
		fread(pic_rgb24,1,w*h*3,fp);
		RGB24_TO_YUV420(pic_rgb24,w,h,pic_yuv420);
		fwrite(pic_yuv420,1,w*h*3/2,fp1);
	}
  
	free(pic_rgb24);
	free(pic_yuv420);
	fclose(fp);
	fclose(fp1);
  
	return 0;
}

调用上面函数的方法如下所示。

simplest_rgb24_to_yuv420("lena_256x256_rgb24.rgb",256,256,1,"output_lena.yuv");

从源代码可以看出,本程序实现了 RGB 到 YUV 的转换公式:

Y= 0.299R+0.587G+0.114*B

U=-0.147R-0.289G+0.463*B

V= 0.615R-0.515G-0.100*B

在转换的过程中有以下几点需要注意: 1) RGB24 存储方式是 Packed,YUV420P 存储方式是 Packed。 2) U,V 在水平和垂直方向的取样数是 Y 的一半

转换前的 RGB24 格式像素数据 lena_256x256_rgb24.rgb 的内容如下所示。

转换后的 YUV420P 格式的像素数据 output_lena.yuv 的内容如下所示。

(11) 生成 RGB24 格式的彩条测试图

本程序中的函数可以生成一张 RGB24 格式的彩条测试图。函数代码如下所示。

/**
 * Generate RGB24 colorbar.
 * @param width    Width of Output RGB file.
 * @param height   Height of Output RGB file.
 * @param url_out  Location of Output RGB file.
 */
int simplest_rgb24_colorbar(int width, int height,char *url_out){
  
	unsigned char *data=NULL;
	int barwidth;
	char filename[100]={0};
	FILE *fp=NULL;
	int i=0,j=0;
  
	data=(unsigned char *)malloc(width*height*3);
	barwidth=width/8;
  
	if((fp=fopen(url_out,"wb+"))==NULL){
		printf("Error: Cannot create file!");
		return -1;
	}
  
	for(j=0;j<height;j++){
		for(i=0;i<width;i++){
			int barnum=i/barwidth;
			switch(barnum){
			case 0:{
				data[(j*width+i)*3+0]=255;
				data[(j*width+i)*3+1]=255;
				data[(j*width+i)*3+2]=255;
				break;
				   }
			case 1:{
				data[(j*width+i)*3+0]=255;
				data[(j*width+i)*3+1]=255;
				data[(j*width+i)*3+2]=0;
				break;
				   }
			case 2:{
				data[(j*width+i)*3+0]=0;
				data[(j*width+i)*3+1]=255;
				data[(j*width+i)*3+2]=255;
				break;
				   }
			case 3:{
				data[(j*width+i)*3+0]=0;
				data[(j*width+i)*3+1]=255;
				data[(j*width+i)*3+2]=0;
				break;
				   }
			case 4:{
				data[(j*width+i)*3+0]=255;
				data[(j*width+i)*3+1]=0;
				data[(j*width+i)*3+2]=255;
				break;
				   }
			case 5:{
				data[(j*width+i)*3+0]=255;
				data[(j*width+i)*3+1]=0;
				data[(j*width+i)*3+2]=0;
				break;
				   }
			case 6:{
				data[(j*width+i)*3+0]=0;
				data[(j*width+i)*3+1]=0;
				data[(j*width+i)*3+2]=255;
  
				break;
				   }
			case 7:{
				data[(j*width+i)*3+0]=0;
				data[(j*width+i)*3+1]=0;
				data[(j*width+i)*3+2]=0;
				break;
				   }
			}
  
		}
	}
	fwrite(data,width*height*3,1,fp);
	fclose(fp);
	free(data);
  
	return 0;
}

调用上面函数的方法如下所示。

simplest_rgb24_colorbar(640, 360,"colorbar_640x360.rgb");

从源代码可以看出,本程序循环输出 “白黄青绿品红蓝黑”8 种颜色的彩条。这 8 种颜色的彩条的 R、G、B 取值如下所示。

生成的图像截图如下所示。  

下载

Simplest mediadata test

项目主页

SourceForge:https://sourceforge.net/projects/simplest-mediadata-test/

Github:https://github.com/leixiaohua1020/simplest_mediadata_test

开源中国:http://git.oschina.net/leixiaohua1020/simplest_mediadata_test CSDN 下载地址:http://download.csdn.net/detail/leixiaohua1020/9422409 本项目包含如下几种视音频数据解析示例:  (1) 像素数据处理程序。包含 RGB 和 YUV 像素格式处理的函数。  (2) 音频采样数据处理程序。包含 PCM 音频采样格式处理的函数。  (3)H.264 码流分析程序。可以分离并解析 NALU。  (4)AAC 码流分析程序。可以分离并解析 ADTS 帧。  (5)FLV 封装格式分析程序。可以将 FLV 中的 MP3 音频码流分离出来。

 (6)UDP-RTP 协议分析程序。可以将分析 UDP/RTP/MPEG-TS 数据包。

雷霄骅 (Lei Xiaohua) leixiaohua1020@126.com http://blog.csdn.net/leixiaohua1020


=====================================================

PCM 音频采样数据处理

上一篇文章记录了 RGB/YUV 视频像素数据的处理方法,本文继续上一篇文章的内容,记录 PCM 音频采样数据的处理方法。音频采样数据在视频播放器的解码流程中的位置如下图所示。

本文分别介绍如下几个 PCM 音频采样数据处理函数:
  分离 PCM16LE 双声道音频采样数据的左声道和右声道
  将 PCM16LE 双声道音频采样数据中左声道的音量降一半
  将 PCM16LE 双声道音频采样数据的声音速度提高一倍
  将 PCM16LE 双声道音频采样数据转换为 PCM8 音频采样数据
  从 PCM16LE 单声道音频采样数据中截取一部分数据
  将 PCM16LE 双声道音频采样数据转换为 WAVE 格式音频数据

注:PCM 音频数据可以使用音频编辑软件导入查看。例如收费的专业音频编辑软件 Adobe Audition,或者免费开源的音频编辑软件 Audacity

函数列表

(1)分离 PCM16LE 双声道音频采样数据的左声道和右声道

本程序中的函数可以将 PCM16LE 双声道数据中左声道和右声道的数据分离成两个文件。函数的代码如下所示。

/**
 * Split Left and Right channel of 16LE PCM file.
 * @param url  Location of PCM file.
 *
 */
int simplest_pcm16le_split(char *url){
	FILE *fp=fopen(url,"rb+");
	FILE *fp1=fopen("output_l.pcm","wb+");
	FILE *fp2=fopen("output_r.pcm","wb+");
  
	unsigned char *sample=(unsigned char *)malloc(4);
  
	while(!feof(fp)){
		fread(sample,1,4,fp);
		//L
		fwrite(sample,1,2,fp1);
		//R
		fwrite(sample+2,1,2,fp2);
	}
  
	free(sample);
	fclose(fp);
	fclose(fp1);
	fclose(fp2);
	return 0;
}

调用上面函数的方法如下所示。

simplest_pcm16le_split("NocturneNo2inEflat_44.1k_s16le.pcm");

从代码可以看出,PCM16LE 双声道数据中左声道和右声道的采样值是间隔存储的。每个采样值占用 2Byte 空间。代码运行后,会把 NocturneNo2inEflat_44.1k_s16le.pcm 的 PCM16LE 格式的数据分离为两个单声道数据:

output_l.pcm:左声道数据。

output_r.pcm:右声道数据。

注:本文中声音样值的采样频率一律是 44100Hz,采样格式一律为 16LE。“16” 代表采样位数是 16bit。由于 1Byte=8bit,所以一个声道的一个采样值占用 2Byte。“LE” 代表 Little Endian,代表 2 Byte 采样值的存储方式为高位存在高地址中。

下图为输入的双声道 PCM 数据的波形图。上面的波形图是左声道的图形,下面的波形图是右声道的波形。图中的横坐标是时间,总长度为 22 秒;纵坐标是取样值,取值范围从 - 32768 到 32767。

下图为分离后左声道数据 output_l.pcm 的音频波形图。

 

下图为分离后右声道数据 output_r.pcm 的音频波形图。

 

(2)将 PCM16LE 双声道音频采样数据中左声道的音量降一半

本程序中的函数可以将 PCM16LE 双声道数据中左声道的音量降低一半。函数的代码如下所示。

/**
 * Halve volume of Left channel of 16LE PCM file
 * @param url  Location of PCM file.
 */
int simplest_pcm16le_halfvolumeleft(char *url){
	FILE *fp=fopen(url,"rb+");
	FILE *fp1=fopen("output_halfleft.pcm","wb+");
  
	int cnt=0;
  
	unsigned char *sample=(unsigned char *)malloc(4);
  
	while(!feof(fp)){
		short *samplenum=NULL;
		fread(sample,1,4,fp);
  
		samplenum=(short *)sample;
		*samplenum=*samplenum/2;
		//L
		fwrite(sample,1,2,fp1);
		//R
		fwrite(sample+2,1,2,fp1);
  
		cnt++;
	}
	printf("Sample Cnt:%d\n",cnt);
  
	free(sample);
	fclose(fp);
	fclose(fp1);
	return 0;
}

调用上面函数的方法如下所示。

simplest_pcm16le_halfvolumeleft("NocturneNo2inEflat_44.1k_s16le.pcm");

从源代码可以看出,本程序在读出左声道的 2 Byte 的取样值之后,将其当成了 C 语言中的一个 short 类型的变量。将该数值除以 2 之后写回到了 PCM 文件中。下图为输入 PCM 双声道音频采样数据的波形图。

下图为输出的左声道经过处理后的波形图。可以看出左声道的波形幅度降低了一半。

(3)将 PCM16LE 双声道音频采样数据的声音速度提高一倍

本程序中的函数可以通过抽象的方式将 PCM16LE 双声道数据的速度提高一倍。函数的代码如下所示。

/**
 * Re-sample to double the speed of 16LE PCM file
 * @param url  Location of PCM file.
 */
int simplest_pcm16le_doublespeed(char *url){
	FILE *fp=fopen(url,"rb+");
	FILE *fp1=fopen("output_doublespeed.pcm","wb+");
  
	int cnt=0;
  
	unsigned char *sample=(unsigned char *)malloc(4);
  
	while(!feof(fp)){
  
		fread(sample,1,4,fp);
  
		if(cnt%2!=0){
			//L
			fwrite(sample,1,2,fp1);
			//R
			fwrite(sample+2,1,2,fp1);
		}
		cnt++;
	}
	printf("Sample Cnt:%d\n",cnt);
  
	free(sample);
	fclose(fp);
	fclose(fp1);
	return 0;
}

调用上面函数的方法如下所示。

simplest_pcm16le_doublespeed("NocturneNo2inEflat_44.1k_s16le.pcm");

从源代码可以看出,本程序只采样了每个声道奇数点的样值。处理完成后,原本 22 秒左右的音频变成了 11 秒左右。音频的播放速度提高了 2 倍,音频的音调也变高了很多。下图为输入 PCM 双声道音频采样数据的波形图。

下图为输出的 PCM 双声道音频采样数据的波形图。通过时间轴可以看出音频变短了很多。

(4)将 PCM16LE 双声道音频采样数据转换为 PCM8 音频采样数据

本程序中的函数可以通过计算的方式将 PCM16LE 双声道数据 16bit 的采样位数转换为 8bit。函数的代码如下所示。

/**
 * Convert PCM-16 data to PCM-8 data.
 * @param url  Location of PCM file.
 */
int simplest_pcm16le_to_pcm8(char *url){
	FILE *fp=fopen(url,"rb+");
	FILE *fp1=fopen("output_8.pcm","wb+");
  
	int cnt=0;
  
	unsigned char *sample=(unsigned char *)malloc(4);
  
	while(!feof(fp)){
  
		short *samplenum16=NULL;
		char samplenum8=0;
		unsigned char samplenum8_u=0;
		fread(sample,1,4,fp);
		//(-32768-32767)
		samplenum16=(short *)sample;
		samplenum8=(*samplenum16)>>8;
		//(0-255)
		samplenum8_u=samplenum8+128;
		//L
		fwrite(&samplenum8_u,1,1,fp1);
  
		samplenum16=(short *)(sample+2);
		samplenum8=(*samplenum16)>>8;
		samplenum8_u=samplenum8+128;
		//R
		fwrite(&samplenum8_u,1,1,fp1);
		cnt++;
	}
	printf("Sample Cnt:%d\n",cnt);
  
	free(sample);
	fclose(fp);
	fclose(fp1);
	return 0;
}

调用上面函数的方法如下所示。

simplest_pcm16le_to_pcm8("NocturneNo2inEflat_44.1k_s16le.pcm");

PCM16LE 格式的采样数据的取值范围是 - 32768 到 32767,而 PCM8 格式的采样数据的取值范围是 0 到 255。所以 PCM16LE 转换到 PCM8 需要经过两个步骤:第一步是将 - 32768 到 32767 的 16bit 有符号数值转换为 - 128 到 127 的 8bit 有符号数值,第二步是将 - 128 到 127 的 8bit 有符号数值转换为 0 到 255 的 8bit 无符号数值。在本程序中,16bit 采样数据是通过 short 类型变量存储的,而 8bit 采样数据是通过 unsigned char 类型存储的。下图为输入的 16bit 的 PCM 双声道音频采样数据的波形图。

下图为输出的 8bit 的 PCM 双声道音频采样数据的波形图。注意观察图中纵坐标的取值范围已经变为 0 至 255。如果仔细聆听声音的话,会发现 8bit PCM 的音质明显不如 16 bit PCM 的音质。
 

(5)将从 PCM16LE 单声道音频采样数据中截取一部分数据

本程序中的函数可以从 PCM16LE 单声道数据中截取一段数据,并输出截取数据的样值。函数的代码如下所示。

/**
 * Cut a 16LE PCM single channel file.
 * @param url        Location of PCM file.
 * @param start_num  start point
 * @param dur_num    how much point to cut
 */
int simplest_pcm16le_cut_singlechannel(char *url,int start_num,int dur_num){
	FILE *fp=fopen(url,"rb+");
	FILE *fp1=fopen("output_cut.pcm","wb+");
	FILE *fp_stat=fopen("output_cut.txt","wb+");
  
	unsigned char *sample=(unsigned char *)malloc(2);
  
	int cnt=0;
	while(!feof(fp)){
		fread(sample,1,2,fp);
		if(cnt>start_num&&cnt<=(start_num+dur_num)){
			fwrite(sample,1,2,fp1);
  
			short samplenum=sample[1];
			samplenum=samplenum*256;
			samplenum=samplenum+sample[0];
  
			fprintf(fp_stat,"%6d,",samplenum);
			if(cnt%10==0)
				fprintf(fp_stat,"\n",samplenum);
		}
		cnt++;
	}
  
	free(sample);
	fclose(fp);
	fclose(fp1);
	fclose(fp_stat);
	return 0;
}

调用上面函数的方法如下所示。

simplest_pcm16le_cut_singlechannel("drum.pcm",2360,120);

本程序可以从 PCM 数据中选取一段采样值保存下来,并且输出这些采样值的数值。上述代码运行后,会把单声道 PCM16LE 格式的 “drum.pcm” 中从 2360 点开始的 120 点的数据保存成 output_cut.pcm 文件。下图为 “drum.pcm” 的波形图,该音频采样频率为 44100KHz,长度为 0.5 秒,一共包含约 22050 个采样点。

下图为截取出来的 output_cut.pcm 文件中的数据。

下面列出了上述数据的采样值。

4460,  5192,  5956,  6680,  7199,  6706,  5727,  4481,  3261,  1993,
  1264,   747,   767,   752,  1248,  1975,  2473,  2955,  2952,  2447,
   974, -1267, -4000, -6965,-10210,-13414,-16639,-19363,-21329,-22541,
-23028,-22545,-21055,-19067,-16829,-14859,-12596, -9900, -6684, -3475,
  -983,  1733,  3978,  5734,  6720,  6978,  6993,  7223,  7225,  7440,
  7688,  8431,  8944,  9468,  9947, 10688, 11194, 11946, 12449, 12446,
 12456, 11974, 11454, 10952, 10167,  9425,  8153,  6941,  5436,  3716,
  1952,   236, -1254, -2463, -3493, -4223, -4695, -4927, -5190, -4941,
 -4188, -2956, -1490,   -40,   705,   932,   446,  -776, -2512, -3994,
 -5723, -7201, -8687,-10157,-11134,-11661,-11642,-11168,-10155, -9142,
 -7888, -7146, -6186, -5694, -4971, -4715, -4498, -4471, -4468, -4452,
 -4452, -3940, -2980, -1984,  -752,   257,  1021,  1264,  1032,    31,

(6)将 PCM16LE 双声道音频采样数据转换为 WAVE 格式音频数据

WAVE 格式音频(扩展名为 “.wav”)是 Windows 系统中最常见的一种音频。该格式的实质就是在 PCM 文件的前面加了一个文件头。本程序的函数就可以通过在 PCM 文件前面加一个 WAVE 文件头从而封装为 WAVE 格式音频。函数的代码如下所示。

/**
 * Convert PCM16LE raw data to WAVE format
 * @param pcmpath      Input PCM file.
 * @param channels     Channel number of PCM file.
 * @param sample_rate  Sample rate of PCM file.
 * @param wavepath     Output WAVE file.
 */
int simplest_pcm16le_to_wave(const char *pcmpath,int channels,int sample_rate,const char *wavepath)
{
  
	typedef struct WAVE_HEADER{  
		char         fccID[4];        
		unsigned   long    dwSize;            
		char         fccType[4];    
	}WAVE_HEADER;  
  
	typedef struct WAVE_FMT{  
		char         fccID[4];        
		unsigned   long       dwSize;            
		unsigned   short     wFormatTag;    
		unsigned   short     wChannels;  
		unsigned   long       dwSamplesPerSec;  
		unsigned   long       dwAvgBytesPerSec;  
		unsigned   short     wBlockAlign;  
		unsigned   short     uiBitsPerSample;  
	}WAVE_FMT;  
  
	typedef struct WAVE_DATA{  
		char       fccID[4];          
		unsigned long dwSize;              
	}WAVE_DATA;  
  
  
	if(channels==0||sample_rate==0){
    channels = 2;
    sample_rate = 44100;
	}
	int bits = 16;
  
    WAVE_HEADER   pcmHEADER;  
    WAVE_FMT   pcmFMT;  
    WAVE_DATA   pcmDATA;  
  
    unsigned   short   m_pcmData;
    FILE   *fp,*fpout;  
  
	fp=fopen(pcmpath, "rb");
    if(fp == NULL) {  
        printf("open pcm file error\n");
        return -1;  
    }
	fpout=fopen(wavepath,   "wb+");
    if(fpout == NULL) {    
        printf("create wav file error\n");  
        return -1; 
    }        
	//WAVE_HEADER
    memcpy(pcmHEADER.fccID,"RIFF",strlen("RIFF"));                    
    memcpy(pcmHEADER.fccType,"WAVE",strlen("WAVE"));  
    fseek(fpout,sizeof(WAVE_HEADER),1); 
	//WAVE_FMT
    pcmFMT.dwSamplesPerSec=sample_rate;  
    pcmFMT.dwAvgBytesPerSec=pcmFMT.dwSamplesPerSec*sizeof(m_pcmData);  
    pcmFMT.uiBitsPerSample=bits;
    memcpy(pcmFMT.fccID,"fmt ",strlen("fmt "));  
    pcmFMT.dwSize=16;  
    pcmFMT.wBlockAlign=2;  
    pcmFMT.wChannels=channels;  
    pcmFMT.wFormatTag=1;  
  
    fwrite(&pcmFMT,sizeof(WAVE_FMT),1,fpout); 
  
    //WAVE_DATA;
    memcpy(pcmDATA.fccID,"data",strlen("data"));  
    pcmDATA.dwSize=0;
    fseek(fpout,sizeof(WAVE_DATA),SEEK_CUR);
  
    fread(&m_pcmData,sizeof(unsigned short),1,fp);
    while(!feof(fp)){  
        pcmDATA.dwSize+=2;
        fwrite(&m_pcmData,sizeof(unsigned short),1,fpout);
        fread(&m_pcmData,sizeof(unsigned short),1,fp);
    }  
  
    pcmHEADER.dwSize=44+pcmDATA.dwSize;
  
    rewind(fpout);
    fwrite(&pcmHEADER,sizeof(WAVE_HEADER),1,fpout);
    fseek(fpout,sizeof(WAVE_FMT),SEEK_CUR);
    fwrite(&pcmDATA,sizeof(WAVE_DATA),1,fpout);
  
	fclose(fp);
    fclose(fpout);
  
    return 0;
}

调用上面函数的方法如下所示。

simplest_pcm16le_to_wave("NocturneNo2inEflat_44.1k_s16le.pcm",2,44100,"output_nocturne.wav");

WAVE 文件是一种 RIFF 格式的文件。其基本块名称是 “WAVE”,其中包含了两个子块“fmt” 和“data”。从编程的角度简单说来就是由 WAVE_HEADER、WAVE_FMT、WAVE_DATA、采样数据共 4 个部分组成。它的结构如下所示。

其中前 3 部分的结构如下所示。在写入 WAVE 文件头的时候给其中的每个字段赋上合适的值就可以了。但是有一点需要注意:WAVE_HEADER 和 WAVE_DATA 中包含了一个文件长度信息的 dwSize 字段,该字段的值必须在写入完音频采样数据之后才能获得。因此这两个结构体最后才写入 WAVE 文件中。

typedef struct WAVE_HEADER{
		char fccID[4];
		unsigned long dwSize;
		char fccType[4];
	}WAVE_HEADER;
  
	typedef struct WAVE_FMT{
		char  fccID[4];
		unsigned long dwSize;
		unsigned short wFormatTag;
		unsigned short wChannels;
		unsigned long dwSamplesPerSec;
		unsigned long dwAvgBytesPerSec;
		unsigned short wBlockAlign;
		unsigned short uiBitsPerSample;
	}WAVE_FMT;
  
	typedef struct WAVE_DATA{
		char       fccID[4];
		unsigned long dwSize;
	}WAVE_DATA;

本程序的函数执行完成后,就可将 NocturneNo2inEflat_44.1k_s16le.pcm 文件封装成 output_nocturne.wav 文件。

下载

Simplest mediadata test

项目主页

SourceForge:https://sourceforge.net/projects/simplest-mediadata-test/

Github:https://github.com/leixiaohua1020/simplest_mediadata_test

开源中国:http://git.oschina.net/leixiaohua1020/simplest_mediadata_test
CSDN 下载地址:http://download.csdn.net/detail/leixiaohua1020/9422409
本项目包含如下几种视音频数据解析示例:
 (1) 像素数据处理程序。包含 RGB 和 YUV 像素格式处理的函数。
 (2) 音频采样数据处理程序。包含 PCM 音频采样格式处理的函数。
 (3)H.264 码流分析程序。可以分离并解析 NALU。
 (4)AAC 码流分析程序。可以分离并解析 ADTS 帧。
 (5)FLV 封装格式分析程序。可以将 FLV 中的 MP3 音频码流分离出来。

 (6)UDP-RTP 协议分析程序。可以将分析 UDP/RTP/MPEG-TS 数据包。

雷霄骅 (Lei Xiaohua)
leixiaohua1020@126.com
http://blog.csdn.net/leixiaohua1020


=====================================================

H.264 视频码流解析

前两篇文章介绍的 YUV/RGB 处理程序以及 PCM 处理程序都属于视音频原始数据的处理程序。从本文开始介绍视音频码流的处理程序。本文介绍的程序是视频码流处理程序。视频码流在视频播放器中的位置如下所示。

本文中的程序是一个 H.264 码流解析程序。该程序可以从 H.264 码流中分析得到它的基本单元 NALU,并且可以简单解析 NALU 首部的字段。通过修改该程序可以实现不同的 H.264 码流处理功能。

原理

H.264 原始码流(又称为 “裸流”)是由一个一个的 NALU 组成的。他们的结构如下图所示。

其中每个 NALU 之间通过 startcode(起始码)进行分隔,起始码分成两种:0x000001(3Byte)或者 0x00000001(4Byte)。如果 NALU 对应的 Slice 为一帧的开始就用 0x00000001,否则就用 0x000001。

H.264 码流解析的步骤就是首先从码流中搜索 0x000001 和 0x00000001,分离出 NALU;然后再分析 NALU 的各个字段。本文的程序即实现了上述的两个步骤。

代码

整个程序位于 simplest_h264_parser() 函数中,如下所示。

/**
 * 最简单的视音频数据处理示例
 * Simplest MediaData Test
 *
 * 雷霄骅 Lei Xiaohua
 * leixiaohua1020@126.com
 * 中国传媒大学/数字电视技术
 * Communication University of China / Digital TV Technology
 * http://blog.csdn.net/leixiaohua1020
 *
 * 本项目包含如下几种视音频测试示例:
 *  (1)像素数据处理程序。包含RGB和YUV像素格式处理的函数。
 *  (2)音频采样数据处理程序。包含PCM音频采样格式处理的函数。
 *  (3)H.264码流分析程序。可以分离并解析NALU。
 *  (4)AAC码流分析程序。可以分离并解析ADTS帧。
 *  (5)FLV封装格式分析程序。可以将FLV中的MP3音频码流分离出来。
 *  (6)UDP-RTP协议分析程序。可以将分析UDP/RTP/MPEG-TS数据包。
 *
 * This project contains following samples to handling multimedia data:
 *  (1) Video pixel data handling program. It contains several examples to handle RGB and YUV data.
 *  (2) Audio sample data handling program. It contains several examples to handle PCM data.
 *  (3) H.264 stream analysis program. It can parse H.264 bitstream and analysis NALU of stream.
 *  (4) AAC stream analysis program. It can parse AAC bitstream and analysis ADTS frame of stream.
 *  (5) FLV format analysis program. It can analysis FLV file and extract MP3 audio stream.
 *  (6) UDP-RTP protocol analysis program. It can analysis UDP/RTP/MPEG-TS Packet.
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
  
typedef enum {
	NALU_TYPE_SLICE    = 1,
	NALU_TYPE_DPA      = 2,
	NALU_TYPE_DPB      = 3,
	NALU_TYPE_DPC      = 4,
	NALU_TYPE_IDR      = 5,
	NALU_TYPE_SEI      = 6,
	NALU_TYPE_SPS      = 7,
	NALU_TYPE_PPS      = 8,
	NALU_TYPE_AUD      = 9,
	NALU_TYPE_EOSEQ    = 10,
	NALU_TYPE_EOSTREAM = 11,
	NALU_TYPE_FILL     = 12,
} NaluType;
  
typedef enum {
	NALU_PRIORITY_DISPOSABLE = 0,
	NALU_PRIRITY_LOW         = 1,
	NALU_PRIORITY_HIGH       = 2,
	NALU_PRIORITY_HIGHEST    = 3
} NaluPriority;
  
  
typedef struct
{
	int startcodeprefix_len;      //! 4 for parameter sets and first slice in picture, 3 for everything else (suggested)
	unsigned len;                 //! Length of the NAL unit (Excluding the start code, which does not belong to the NALU)
	unsigned max_size;            //! Nal Unit Buffer size
	int forbidden_bit;            //! should be always FALSE
	int nal_reference_idc;        //! NALU_PRIORITY_xxxx
	int nal_unit_type;            //! NALU_TYPE_xxxx    
	char *buf;                    //! contains the first byte followed by the EBSP
} NALU_t;
  
FILE *h264bitstream = NULL;                //!< the bit stream file
  
int info2=0, info3=0;
  
static int FindStartCode2 (unsigned char *Buf){
	if(Buf[0]!=0 || Buf[1]!=0 || Buf[2] !=1) return 0; //0x000001?
	else return 1;
}
  
static int FindStartCode3 (unsigned char *Buf){
	if(Buf[0]!=0 || Buf[1]!=0 || Buf[2] !=0 || Buf[3] !=1) return 0;//0x00000001?
	else return 1;
}
  
  
int GetAnnexbNALU (NALU_t *nalu){
	int pos = 0;
	int StartCodeFound, rewind;
	unsigned char *Buf;
  
	if ((Buf = (unsigned char*)calloc (nalu->max_size , sizeof(char))) == NULL) 
		printf ("GetAnnexbNALU: Could not allocate Buf memory\n");
  
	nalu->startcodeprefix_len=3;
  
	if (3 != fread (Buf, 1, 3, h264bitstream)){
		free(Buf);
		return 0;
	}
	info2 = FindStartCode2 (Buf);
	if(info2 != 1) {
		if(1 != fread(Buf+3, 1, 1, h264bitstream)){
			free(Buf);
			return 0;
		}
		info3 = FindStartCode3 (Buf);
		if (info3 != 1){ 
			free(Buf);
			return -1;
		}
		else {
			pos = 4;
			nalu->startcodeprefix_len = 4;
		}
	}
	else{
		nalu->startcodeprefix_len = 3;
		pos = 3;
	}
	StartCodeFound = 0;
	info2 = 0;
	info3 = 0;
  
	while (!StartCodeFound){
		if (feof (h264bitstream)){
			nalu->len = (pos-1)-nalu->startcodeprefix_len;
			memcpy (nalu->buf, &Buf[nalu->startcodeprefix_len], nalu->len);     
			nalu->forbidden_bit = nalu->buf[0] & 0x80; //1 bit
			nalu->nal_reference_idc = nalu->buf[0] & 0x60; // 2 bit
			nalu->nal_unit_type = (nalu->buf[0]) & 0x1f;// 5 bit
			free(Buf);
			return pos-1;
		}
		Buf[pos++] = fgetc (h264bitstream);
		info3 = FindStartCode3(&Buf[pos-4]);
		if(info3 != 1)
			info2 = FindStartCode2(&Buf[pos-3]);
		StartCodeFound = (info2 == 1 || info3 == 1);
	}
  
	// Here, we have found another start code (and read length of startcode bytes more than we should
	// have.  Hence, go back in the file
	rewind = (info3 == 1)? -4 : -3;
  
	if (0 != fseek (h264bitstream, rewind, SEEK_CUR)){
		free(Buf);
		printf("GetAnnexbNALU: Cannot fseek in the bit stream file");
	}
  
	// Here the Start code, the complete NALU, and the next start code is in the Buf.  
	// The size of Buf is pos, pos+rewind are the number of bytes excluding the next
	// start code, and (pos+rewind)-startcodeprefix_len is the size of the NALU excluding the start code
  
	nalu->len = (pos+rewind)-nalu->startcodeprefix_len;
	memcpy (nalu->buf, &Buf[nalu->startcodeprefix_len], nalu->len);//
	nalu->forbidden_bit = nalu->buf[0] & 0x80; //1 bit
	nalu->nal_reference_idc = nalu->buf[0] & 0x60; // 2 bit
	nalu->nal_unit_type = (nalu->buf[0]) & 0x1f;// 5 bit
	free(Buf);
  
	return (pos+rewind);
}
  
/**
 * Analysis H.264 Bitstream
 * @param url    Location of input H.264 bitstream file.
 */
int simplest_h264_parser(char *url){
  
	NALU_t *n;
	int buffersize=100000;
  
	//FILE *myout=fopen("output_log.txt","wb+");
	FILE *myout=stdout;
  
	h264bitstream=fopen(url, "rb+");
	if (h264bitstream==NULL){
		printf("Open file error\n");
		return 0;
	}
  
	n = (NALU_t*)calloc (1, sizeof (NALU_t));
	if (n == NULL){
		printf("Alloc NALU Error\n");
		return 0;
	}
  
	n->max_size=buffersize;
	n->buf = (char*)calloc (buffersize, sizeof (char));
	if (n->buf == NULL){
		free (n);
		printf ("AllocNALU: n->buf");
		return 0;
	}
  
	int data_offset=0;
	int nal_num=0;
	printf("-----+-------- NALU Table ------+---------+\n");
	printf(" NUM |    POS  |    IDC |  TYPE |   LEN   |\n");
	printf("-----+---------+--------+-------+---------+\n");
  
	while(!feof(h264bitstream)) 
	{
		int data_lenth;
		data_lenth=GetAnnexbNALU(n);
  
		char type_str[20]={0};
		switch(n->nal_unit_type){
			case NALU_TYPE_SLICE:sprintf(type_str,"SLICE");break;
			case NALU_TYPE_DPA:sprintf(type_str,"DPA");break;
			case NALU_TYPE_DPB:sprintf(type_str,"DPB");break;
			case NALU_TYPE_DPC:sprintf(type_str,"DPC");break;
			case NALU_TYPE_IDR:sprintf(type_str,"IDR");break;
			case NALU_TYPE_SEI:sprintf(type_str,"SEI");break;
			case NALU_TYPE_SPS:sprintf(type_str,"SPS");break;
			case NALU_TYPE_PPS:sprintf(type_str,"PPS");break;
			case NALU_TYPE_AUD:sprintf(type_str,"AUD");break;
			case NALU_TYPE_EOSEQ:sprintf(type_str,"EOSEQ");break;
			case NALU_TYPE_EOSTREAM:sprintf(type_str,"EOSTREAM");break;
			case NALU_TYPE_FILL:sprintf(type_str,"FILL");break;
		}
		char idc_str[20]={0};
		switch(n->nal_reference_idc>>5){
			case NALU_PRIORITY_DISPOSABLE:sprintf(idc_str,"DISPOS");break;
			case NALU_PRIRITY_LOW:sprintf(idc_str,"LOW");break;
			case NALU_PRIORITY_HIGH:sprintf(idc_str,"HIGH");break;
			case NALU_PRIORITY_HIGHEST:sprintf(idc_str,"HIGHEST");break;
		}
  
		fprintf(myout,"%5d| %8d| %7s| %6s| %8d|\n",nal_num,data_offset,idc_str,type_str,n->len);
  
		data_offset=data_offset+data_lenth;
  
		nal_num++;
	}
  
	//Free
	if (n){
		if (n->buf){
			free(n->buf);
			n->buf=NULL;
		}
		free (n);
	}
	return 0;
}

上文中的函数调用方法如下所示。

simplest_h264_parser("sintel.h264");

结果

本程序的输入为一个 H.264 原始码流(裸流)的文件路径,输出为该码流的 NALU 统计数据,如下图所示。

下载

Simplest mediadata test

项目主页

SourceForge:https://sourceforge.net/projects/simplest-mediadata-test/

Github:https://github.com/leixiaohua1020/simplest_mediadata_test

开源中国:http://git.oschina.net/leixiaohua1020/simplest_mediadata_test

CSDN 下载地址:http://download.csdn.net/detail/leixiaohua1020/9422409

本项目包含如下几种视音频数据解析示例:

 (1) 像素数据处理程序。包含 RGB 和 YUV 像素格式处理的函数。
 (2) 音频采样数据处理程序。包含 PCM 音频采样格式处理的函数。
 (3)H.264 码流分析程序。可以分离并解析 NALU。
 (4)AAC 码流分析程序。可以分离并解析 ADTS 帧。
 (5)FLV 封装格式分析程序。可以将 FLV 中的 MP3 音频码流分离出来。
 (6)UDP-RTP 协议分析程序。可以将分析 UDP/RTP/MPEG-TS 数据包。

雷霄骅 (Lei Xiaohua)
leixiaohua1020@126.com
http://blog.csdn.net/leixiaohua1020


=====================================================

AAC 音频码流解析

本文继续上一篇文章的内容,介绍一个音频码流处理程序。音频码流在视频播放器中的位置如下所示。

本文中的程序是一个 AAC 码流解析程序。该程序可以从 AAC 码流中分析得到它的基本单元 ADTS frame,并且可以简单解析 ADTS frame 首部的字段。通过修改该程序可以实现不同的 AAC 码流处理功能。

原理

AAC 原始码流(又称为 “裸流”)是由一个一个的 ADTS frame 组成的。他们的结构如下图所示。

其中每个 ADTS frame 之间通过 syncword(同步字)进行分隔。同步字为 0xFFF(二进制 “111111111111”)。AAC 码流解析的步骤就是首先从码流中搜索 0x0FFF,分离出 ADTS frame;然后再分析 ADTS frame 的首部各个字段。本文的程序即实现了上述的两个步骤。

代码

整个程序位于 simplest_aac_parser() 函数中,如下所示。

/**
 * 最简单的视音频数据处理示例
 * Simplest MediaData Test
 *
 * 雷霄骅 Lei Xiaohua
 * leixiaohua1020@126.com
 * 中国传媒大学/数字电视技术
 * Communication University of China / Digital TV Technology
 * http://blog.csdn.net/leixiaohua1020
 *
 * 本项目包含如下几种视音频测试示例:
 *  (1)像素数据处理程序。包含RGB和YUV像素格式处理的函数。
 *  (2)音频采样数据处理程序。包含PCM音频采样格式处理的函数。
 *  (3)H.264码流分析程序。可以分离并解析NALU。
 *  (4)AAC码流分析程序。可以分离并解析ADTS帧。
 *  (5)FLV封装格式分析程序。可以将FLV中的MP3音频码流分离出来。
 *  (6)UDP-RTP协议分析程序。可以将分析UDP/RTP/MPEG-TS数据包。
 *
 * This project contains following samples to handling multimedia data:
 *  (1) Video pixel data handling program. It contains several examples to handle RGB and YUV data.
 *  (2) Audio sample data handling program. It contains several examples to handle PCM data.
 *  (3) H.264 stream analysis program. It can parse H.264 bitstream and analysis NALU of stream.
 *  (4) AAC stream analysis program. It can parse AAC bitstream and analysis ADTS frame of stream.
 *  (5) FLV format analysis program. It can analysis FLV file and extract MP3 audio stream.
 *  (6) UDP-RTP protocol analysis program. It can analysis UDP/RTP/MPEG-TS Packet.
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
  
  
int getADTSframe(unsigned char* buffer, int buf_size, unsigned char* data ,int* data_size){
	int size = 0;
  
	if(!buffer || !data || !data_size ){
		return -1;
	}
  
	while(1){
		if(buf_size  < 7 ){
			return -1;
		}
		//Sync words
		if((buffer[0] == 0xff) && ((buffer[1] & 0xf0) == 0xf0) ){
			size |= ((buffer[3] & 0x03) <<11);     //high 2 bit
			size |= buffer[4]<<3;                //middle 8 bit
			size |= ((buffer[5] & 0xe0)>>5);        //low 3bit
			break;
		}
		--buf_size;
		++buffer;
	}
  
	if(buf_size < size){
		return 1;
	}
  
	memcpy(data, buffer, size);
	*data_size = size;
  
	return 0;
}
  
int simplest_aac_parser(char *url)
{
	int data_size = 0;
	int size = 0;
	int cnt=0;
	int offset=0;
  
	//FILE *myout=fopen("output_log.txt","wb+");
	FILE *myout=stdout;
  
	unsigned char *aacframe=(unsigned char *)malloc(1024*5);
	unsigned char *aacbuffer=(unsigned char *)malloc(1024*1024);
  
	FILE *ifile = fopen(url, "rb");
	if(!ifile){
		printf("Open file error");
		return -1;
	}
  
	printf("-----+- ADTS Frame Table -+------+\n");
	printf(" NUM | Profile | Frequency| Size |\n");
	printf("-----+---------+----------+------+\n");
  
	while(!feof(ifile)){
		data_size = fread(aacbuffer+offset, 1, 1024*1024-offset, ifile);
		unsigned char* input_data = aacbuffer;
  
		while(1)
		{
			int ret=getADTSframe(input_data, data_size, aacframe, &size);
			if(ret==-1){
				break;
			}else if(ret==1){
				memcpy(aacbuffer,input_data,data_size);
				offset=data_size;
				break;
			}
  
			char profile_str[10]={0};
			char frequence_str[10]={0};
  
			unsigned char profile=aacframe[2]&0xC0;
			profile=profile>>6;
			switch(profile){
			case 0: sprintf(profile_str,"Main");break;
			case 1: sprintf(profile_str,"LC");break;
			case 2: sprintf(profile_str,"SSR");break;
			default:sprintf(profile_str,"unknown");break;
			}
  
			unsigned char sampling_frequency_index=aacframe[2]&0x3C;
			sampling_frequency_index=sampling_frequency_index>>2;
			switch(sampling_frequency_index){
			case 0: sprintf(frequence_str,"96000Hz");break;
			case 1: sprintf(frequence_str,"88200Hz");break;
			case 2: sprintf(frequence_str,"64000Hz");break;
			case 3: sprintf(frequence_str,"48000Hz");break;
			case 4: sprintf(frequence_str,"44100Hz");break;
			case 5: sprintf(frequence_str,"32000Hz");break;
			case 6: sprintf(frequence_str,"24000Hz");break;
			case 7: sprintf(frequence_str,"22050Hz");break;
			case 8: sprintf(frequence_str,"16000Hz");break;
			case 9: sprintf(frequence_str,"12000Hz");break;
			case 10: sprintf(frequence_str,"11025Hz");break;
			case 11: sprintf(frequence_str,"8000Hz");break;
			default:sprintf(frequence_str,"unknown");break;
			}
  
  
			fprintf(myout,"%5d| %8s|  %8s| %5d|\n",cnt,profile_str ,frequence_str,size);
			data_size -= size;
			input_data += size;
			cnt++;
		}   
  
	}
	fclose(ifile);
	free(aacbuffer);
	free(aacframe);
  
	return 0;
}

上文中的函数调用方法如下所示。

simplest_aac_parser("nocturne.aac");

结果

本程序的输入为一个 AAC 原始码流(裸流)的文件路径,输出为该码流中 ADTS frame 的统计数据,如下图所示。

下载

Simplest mediadata test

项目主页

SourceForge:https://sourceforge.net/projects/simplest-mediadata-test/

Github:https://github.com/leixiaohua1020/simplest_mediadata_test

开源中国:http://git.oschina.net/leixiaohua1020/simplest_mediadata_test
CSDN 下载地址:http://download.csdn.net/detail/leixiaohua1020/9422409
本项目包含如下几种视音频数据解析示例:
 (1) 像素数据处理程序。包含 RGB 和 YUV 像素格式处理的函数。
 (2) 音频采样数据处理程序。包含 PCM 音频采样格式处理的函数。
 (3)H.264 码流分析程序。可以分离并解析 NALU。
 (4)AAC 码流分析程序。可以分离并解析 ADTS 帧。
 (5)FLV 封装格式分析程序。可以将 FLV 中的 MP3 音频码流分离出来。
 (6)UDP-RTP 协议分析程序。可以将分析 UDP/RTP/MPEG-TS 数据包。
雷霄骅 (Lei Xiaohua)
leixiaohua1020@126.com
http://blog.csdn.net/leixiaohua1020


=====================================================

FLV 封装格式解析

前两篇文章介绍了音频码流处理程序和视频码流处理程序,本文介绍将他们打包到一起后的数据——封装格式数据的处理程序。封装格式数据在视频播放器中的位置如下所示。

本文中的程序是一个 FLV 封装格式解析程序。该程序可以从 FLV 中分析得到它的基本单元 Tag,并且可以简单解析 Tag 首部的字段。通过修改该程序可以实现不同的 FLV 格式数据处理功能。

原理

FLV 封装格式是由一个 FLV Header 文件头和一个一个的 Tag 组成的。Tag 中包含了音频数据以及视频数据。FLV 的结构如下图所示。

有关 FLV 的格式本文不再做记录。可以参考文章《视音频编解码学习工程:FLV 封装格式分析器》。本文的程序实现了 FLV 中的 FLV Header 和 Tag 的解析,并可以分离出其中的音频流。

代码

整个程序位于 simplest_flv_parser() 函数中,如下所示。

/**
 * 最简单的视音频数据处理示例
 * Simplest MediaData Test
 *
 * 雷霄骅 Lei Xiaohua
 * leixiaohua1020@126.com
 * 中国传媒大学/数字电视技术
 * Communication University of China / Digital TV Technology
 * http://blog.csdn.net/leixiaohua1020
 *
 * 本项目包含如下几种视音频测试示例:
 *  (1)像素数据处理程序。包含RGB和YUV像素格式处理的函数。
 *  (2)音频采样数据处理程序。包含PCM音频采样格式处理的函数。
 *  (3)H.264码流分析程序。可以分离并解析NALU。
 *  (4)AAC码流分析程序。可以分离并解析ADTS帧。
 *  (5)FLV封装格式分析程序。可以将FLV中的MP3音频码流分离出来。
 *  (6)UDP-RTP协议分析程序。可以将分析UDP/RTP/MPEG-TS数据包。
 *
 * This project contains following samples to handling multimedia data:
 *  (1) Video pixel data handling program. It contains several examples to handle RGB and YUV data.
 *  (2) Audio sample data handling program. It contains several examples to handle PCM data.
 *  (3) H.264 stream analysis program. It can parse H.264 bitstream and analysis NALU of stream.
 *  (4) AAC stream analysis program. It can parse AAC bitstream and analysis ADTS frame of stream.
 *  (5) FLV format analysis program. It can analysis FLV file and extract MP3 audio stream.
 *  (6) UDP-RTP protocol analysis program. It can analysis UDP/RTP/MPEG-TS Packet.
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
  
//Important!
#pragma pack(1)
  
  
#define TAG_TYPE_SCRIPT 18
#define TAG_TYPE_AUDIO  8
#define TAG_TYPE_VIDEO  9
  
typedef unsigned char byte;
typedef unsigned int uint;
  
typedef struct {
	byte Signature[3];
	byte Version;
	byte Flags;
	uint DataOffset;
} FLV_HEADER;
  
typedef struct {
	byte TagType;
	byte DataSize[3];
	byte Timestamp[3];
	uint Reserved;
} TAG_HEADER;
  
  
//reverse_bytes - turn a BigEndian byte array into a LittleEndian integer
uint reverse_bytes(byte *p, char c) {
	int r = 0;
	int i;
	for (i=0; i<c; i++) 
		r |= ( *(p+i) << (((c-1)*8)-8*i));
	return r;
}
  
/**
 * Analysis FLV file
 * @param url    Location of input FLV file.
 */
  
int simplest_flv_parser(char *url){
  
	//whether output audio/video stream
	int output_a=1;
	int output_v=1;
	//-------------
	FILE *ifh=NULL,*vfh=NULL, *afh = NULL;
  
	//FILE *myout=fopen("output_log.txt","wb+");
	FILE *myout=stdout;
  
	FLV_HEADER flv;
	TAG_HEADER tagheader;
	uint previoustagsize, previoustagsize_z=0;
	uint ts=0, ts_new=0;
  
	ifh = fopen(url, "rb+");
	if ( ifh== NULL) {
		printf("Failed to open files!");
		return -1;
	}
  
	//FLV file header
	fread((char *)&flv,1,sizeof(FLV_HEADER),ifh);
  
	fprintf(myout,"============== FLV Header ==============\n");
	fprintf(myout,"Signature:  0x %c %c %c\n",flv.Signature[0],flv.Signature[1],flv.Signature[2]);
	fprintf(myout,"Version:    0x %X\n",flv.Version);
	fprintf(myout,"Flags  :    0x %X\n",flv.Flags);
	fprintf(myout,"HeaderSize: 0x %X\n",reverse_bytes((byte *)&flv.DataOffset, sizeof(flv.DataOffset)));
	fprintf(myout,"========================================\n");
  
	//move the file pointer to the end of the header
	fseek(ifh, reverse_bytes((byte *)&flv.DataOffset, sizeof(flv.DataOffset)), SEEK_SET);
  
	//process each tag
	do {
  
		previoustagsize = _getw(ifh);
  
		fread((void *)&tagheader,sizeof(TAG_HEADER),1,ifh);
  
		//int temp_datasize1=reverse_bytes((byte *)&tagheader.DataSize, sizeof(tagheader.DataSize));
		int tagheader_datasize=tagheader.DataSize[0]*65536+tagheader.DataSize[1]*256+tagheader.DataSize[2];
		int tagheader_timestamp=tagheader.Timestamp[0]*65536+tagheader.Timestamp[1]*256+tagheader.Timestamp[2];
  
		char tagtype_str[10];
		switch(tagheader.TagType){
		case TAG_TYPE_AUDIO:sprintf(tagtype_str,"AUDIO");break;
		case TAG_TYPE_VIDEO:sprintf(tagtype_str,"VIDEO");break;
		case TAG_TYPE_SCRIPT:sprintf(tagtype_str,"SCRIPT");break;
		default:sprintf(tagtype_str,"UNKNOWN");break;
		}
		fprintf(myout,"[%6s] %6d %6d |",tagtype_str,tagheader_datasize,tagheader_timestamp);
  
		//if we are not past the end of file, process the tag
		if (feof(ifh)) {
			break;
		}
  
		//process tag by type
		switch (tagheader.TagType) {
  
		case TAG_TYPE_AUDIO:{ 
			char audiotag_str[100]={0};
			strcat(audiotag_str,"| ");
			char tagdata_first_byte;
			tagdata_first_byte=fgetc(ifh);
			int x=tagdata_first_byte&0xF0;
			x=x>>4;
			switch (x)
			{
			case 0:strcat(audiotag_str,"Linear PCM, platform endian");break;
			case 1:strcat(audiotag_str,"ADPCM");break;
			case 2:strcat(audiotag_str,"MP3");break;
			case 3:strcat(audiotag_str,"Linear PCM, little endian");break;
			case 4:strcat(audiotag_str,"Nellymoser 16-kHz mono");break;
			case 5:strcat(audiotag_str,"Nellymoser 8-kHz mono");break;
			case 6:strcat(audiotag_str,"Nellymoser");break;
			case 7:strcat(audiotag_str,"G.711 A-law logarithmic PCM");break;
			case 8:strcat(audiotag_str,"G.711 mu-law logarithmic PCM");break;
			case 9:strcat(audiotag_str,"reserved");break;
			case 10:strcat(audiotag_str,"AAC");break;
			case 11:strcat(audiotag_str,"Speex");break;
			case 14:strcat(audiotag_str,"MP3 8-Khz");break;
			case 15:strcat(audiotag_str,"Device-specific sound");break;
			default:strcat(audiotag_str,"UNKNOWN");break;
			}
			strcat(audiotag_str,"| ");
			x=tagdata_first_byte&0x0C;
			x=x>>2;
			switch (x)
			{
			case 0:strcat(audiotag_str,"5.5-kHz");break;
			case 1:strcat(audiotag_str,"1-kHz");break;
			case 2:strcat(audiotag_str,"22-kHz");break;
			case 3:strcat(audiotag_str,"44-kHz");break;
			default:strcat(audiotag_str,"UNKNOWN");break;
			}
			strcat(audiotag_str,"| ");
			x=tagdata_first_byte&0x02;
			x=x>>1;
			switch (x)
			{
			case 0:strcat(audiotag_str,"8Bit");break;
			case 1:strcat(audiotag_str,"16Bit");break;
			default:strcat(audiotag_str,"UNKNOWN");break;
			}
			strcat(audiotag_str,"| ");
			x=tagdata_first_byte&0x01;
			switch (x)
			{
			case 0:strcat(audiotag_str,"Mono");break;
			case 1:strcat(audiotag_str,"Stereo");break;
			default:strcat(audiotag_str,"UNKNOWN");break;
			}
			fprintf(myout,"%s",audiotag_str);
  
			//if the output file hasn't been opened, open it.
			if(output_a!=0&&afh == NULL){
				afh = fopen("output.mp3", "wb");
			}
  
			//TagData - First Byte Data
			int data_size=reverse_bytes((byte *)&tagheader.DataSize, sizeof(tagheader.DataSize))-1;
			if(output_a!=0){
				//TagData+1
				for (int i=0; i<data_size; i++)
					fputc(fgetc(ifh),afh);
  
			}else{
				for (int i=0; i<data_size; i++)
					fgetc(ifh);
			}
			break;
		}
		case TAG_TYPE_VIDEO:{
			char videotag_str[100]={0};
			strcat(videotag_str,"| ");
			char tagdata_first_byte;
			tagdata_first_byte=fgetc(ifh);
			int x=tagdata_first_byte&0xF0;
			x=x>>4;
			switch (x)
			{
			case 1:strcat(videotag_str,"key frame  ");break;
			case 2:strcat(videotag_str,"inter frame");break;
			case 3:strcat(videotag_str,"disposable inter frame");break;
			case 4:strcat(videotag_str,"generated keyframe");break;
			case 5:strcat(videotag_str,"video info/command frame");break;
			default:strcat(videotag_str,"UNKNOWN");break;
			}
			strcat(videotag_str,"| ");
			x=tagdata_first_byte&0x0F;
			switch (x)
			{
			case 1:strcat(videotag_str,"JPEG (currently unused)");break;
			case 2:strcat(videotag_str,"Sorenson H.263");break;
			case 3:strcat(videotag_str,"Screen video");break;
			case 4:strcat(videotag_str,"On2 VP6");break;
			case 5:strcat(videotag_str,"On2 VP6 with alpha channel");break;
			case 6:strcat(videotag_str,"Screen video version 2");break;
			case 7:strcat(videotag_str,"AVC");break;
			default:strcat(videotag_str,"UNKNOWN");break;
			}
			fprintf(myout,"%s",videotag_str);
  
			fseek(ifh, -1, SEEK_CUR);
			//if the output file hasn't been opened, open it.
			if (vfh == NULL&&output_v!=0) {
				//write the flv header (reuse the original file's hdr) and first previoustagsize
					vfh = fopen("output.flv", "wb");
					fwrite((char *)&flv,1, sizeof(flv),vfh);
					fwrite((char *)&previoustagsize_z,1,sizeof(previoustagsize_z),vfh);
			}
#if 0
			//Change Timestamp
			//Get Timestamp
			ts = reverse_bytes((byte *)&tagheader.Timestamp, sizeof(tagheader.Timestamp));
			ts=ts*2;
			//Writeback Timestamp
			ts_new = reverse_bytes((byte *)&ts, sizeof(ts));
			memcpy(&tagheader.Timestamp, ((char *)&ts_new) + 1, sizeof(tagheader.Timestamp));
#endif
  
  
			//TagData + Previous Tag Size
			int data_size=reverse_bytes((byte *)&tagheader.DataSize, sizeof(tagheader.DataSize))+4;
			if(output_v!=0){
				//TagHeader
				fwrite((char *)&tagheader,1, sizeof(tagheader),vfh);
				//TagData
				for (int i=0; i<data_size; i++)
					fputc(fgetc(ifh),vfh);
			}else{
				for (int i=0; i<data_size; i++)
					fgetc(ifh);
			}
			//rewind 4 bytes, because we need to read the previoustagsize again for the loop's sake
			fseek(ifh, -4, SEEK_CUR);
  
			break;
			}
		default:
  
			//skip the data of this tag
			fseek(ifh, reverse_bytes((byte *)&tagheader.DataSize, sizeof(tagheader.DataSize)), SEEK_CUR);
		}
  
		fprintf(myout,"\n");
  
	} while (!feof(ifh));
  
  
	_fcloseall();
  
	return 0;
}

上文中的函数调用方法如下所示。

simplest_flv_parser("cuc_ieschool.flv");

结果

本程序的输入为一个 FLV 的文件路径,输出为 FLV 的统计数据,如下图所示。

此外本程序还可以分离 FLV 中的视频码流和音频码流。需要注意的是本程序并不能分离一些特定类型的音频(例如 AAC)和视频,这一工作有待以后有时间再完成。

下载

Simplest mediadata test

项目主页

SourceForge:https://sourceforge.net/projects/simplest-mediadata-test/

Github:https://github.com/leixiaohua1020/simplest_mediadata_test

开源中国:http://git.oschina.net/leixiaohua1020/simplest_mediadata_test
CSDN 下载地址:http://download.csdn.net/detail/leixiaohua1020/9422409
本项目包含如下几种视音频数据解析示例:
 (1) 像素数据处理程序。包含 RGB 和 YUV 像素格式处理的函数。
 (2) 音频采样数据处理程序。包含 PCM 音频采样格式处理的函数。
 (3)H.264 码流分析程序。可以分离并解析 NALU。
 (4)AAC 码流分析程序。可以分离并解析 ADTS 帧。
 (5)FLV 封装格式分析程序。可以将 FLV 中的 MP3 音频码流分离出来。
 (6)UDP-RTP 协议分析程序。可以将分析 UDP/RTP/MPEG-TS 数据包。
雷霄骅 (Lei Xiaohua)
leixiaohua1020@126.com
http://blog.csdn.net/leixiaohua1020


=====================================================

UDP-RTP 协议解析

本文介绍网络协议数据的处理程序。网络协议数据在视频播放器中的位置如下所示。

本文中的程序是一个 UDP/RTP 协议流媒体数据解析器。该程序可以分析 UDP 协议中的 RTP 包头中的内容,以及 RTP 负载中 MPEG-TS 封装格式的信息。通过修改该程序可以实现不同的 UDP/RTP 协议数据处理功能。

原理

MPEG-TS 封装格式数据打包为 RTP/UDP 协议然后发送出去的流程如下图所示。图中首先每 7 个 MPEG-TS Packet 打包为一个 RTP,然后每个 RTP 再打包为一个 UDP。其中打包 RTP 的方法就是在 MPEG-TS 数据前面加上 RTP Header,而打包 RTP 的方法就是在 RTP 数据前面加上 UDP Header。

有关 MPEG-TS、RTP、UDP 的知识不再详细介绍,可以参考相关的文档了解其中的细节信息。本文记录的程序是一个收取流媒体的程序,因此本文程序的流程和上述发送 MPEG-TS 的流程正好是相反的。该程序可以通过 Socket 编程收取 UDP 包,解析其中的 RTP 包的信息,然后再解析 RTP 包中 MPEG-TS Packet 的信息。

代码

整个程序位于 simplest_udp_parser() 函数中,如下所示。

/**
 * 最简单的视音频数据处理示例
 * Simplest MediaData Test
 *
 * 雷霄骅 Lei Xiaohua
 * leixiaohua1020@126.com
 * 中国传媒大学/数字电视技术
 * Communication University of China / Digital TV Technology
 * http://blog.csdn.net/leixiaohua1020
 *
 * 本项目包含如下几种视音频测试示例:
 *  (1)像素数据处理程序。包含RGB和YUV像素格式处理的函数。
 *  (2)音频采样数据处理程序。包含PCM音频采样格式处理的函数。
 *  (3)H.264码流分析程序。可以分离并解析NALU。
 *  (4)AAC码流分析程序。可以分离并解析ADTS帧。
 *  (5)FLV封装格式分析程序。可以将FLV中的MP3音频码流分离出来。
 *  (6)UDP-RTP协议分析程序。可以将分析UDP/RTP/MPEG-TS数据包。
 *
 * This project contains following samples to handling multimedia data:
 *  (1) Video pixel data handling program. It contains several examples to handle RGB and YUV data.
 *  (2) Audio sample data handling program. It contains several examples to handle PCM data.
 *  (3) H.264 stream analysis program. It can parse H.264 bitstream and analysis NALU of stream.
 *  (4) AAC stream analysis program. It can parse AAC bitstream and analysis ADTS frame of stream.
 *  (5) FLV format analysis program. It can analysis FLV file and extract MP3 audio stream.
 *  (6) UDP-RTP protocol analysis program. It can analysis UDP/RTP/MPEG-TS Packet.
 *
 */
#include <stdio.h>
#include <winsock2.h>
  
#pragma comment(lib, "ws2_32.lib") 
  
#pragma pack(1)
  
/*
 * [memo] FFmpeg stream Command:
 * ffmpeg -re -i sintel.ts -f mpegts udp://127.0.0.1:8880
 * ffmpeg -re -i sintel.ts -f rtp_mpegts udp://127.0.0.1:8880
 */
  
typedef struct RTP_FIXED_HEADER{
	/* byte 0 */
	unsigned char csrc_len:4;       /* expect 0 */
	unsigned char extension:1;      /* expect 1 */
	unsigned char padding:1;        /* expect 0 */
	unsigned char version:2;        /* expect 2 */
	/* byte 1 */
	unsigned char payload:7;
	unsigned char marker:1;        /* expect 1 */
	/* bytes 2, 3 */
	unsigned short seq_no;            
	/* bytes 4-7 */
	unsigned  long timestamp;        
	/* bytes 8-11 */
	unsigned long ssrc;            /* stream number is used here. */
} RTP_FIXED_HEADER;
  
typedef struct MPEGTS_FIXED_HEADER {
	unsigned sync_byte: 8; 
	unsigned transport_error_indicator: 1; 
	unsigned payload_unit_start_indicator: 1;
	unsigned transport_priority: 1; 
	unsigned PID: 13;
	unsigned scrambling_control: 2;
	unsigned adaptation_field_exist: 2;
	unsigned continuity_counter: 4;
} MPEGTS_FIXED_HEADER;
  
  
  
int simplest_udp_parser(int port)
{
	WSADATA wsaData;
	WORD sockVersion = MAKEWORD(2,2);
	int cnt=0;
  
	//FILE *myout=fopen("output_log.txt","wb+");
	FILE *myout=stdout;
  
	FILE *fp1=fopen("output_dump.ts","wb+");
  
	if(WSAStartup(sockVersion, &wsaData) != 0){
		return 0;
	}
  
	SOCKET serSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); 
	if(serSocket == INVALID_SOCKET){
		printf("socket error !");
		return 0;
	}
  
	sockaddr_in serAddr;
	serAddr.sin_family = AF_INET;
	serAddr.sin_port = htons(port);
	serAddr.sin_addr.S_un.S_addr = INADDR_ANY;
	if(bind(serSocket, (sockaddr *)&serAddr, sizeof(serAddr)) == SOCKET_ERROR){
		printf("bind error !");
		closesocket(serSocket);
		return 0;
	}
  
	sockaddr_in remoteAddr;
	int nAddrLen = sizeof(remoteAddr); 
  
	//How to parse?
	int parse_rtp=1;
	int parse_mpegts=1;
  
	printf("Listening on port %d\n",port);
  
	char recvData[10000];  
	while (1){
  
		int pktsize = recvfrom(serSocket, recvData, 10000, 0, (sockaddr *)&remoteAddr, &nAddrLen);
		if (pktsize > 0){
			//printf("Addr:%s\r\n",inet_ntoa(remoteAddr.sin_addr));
			//printf("packet size:%d\r\n",pktsize);
			//Parse RTP
			//
			if(parse_rtp!=0){
				char payload_str[10]={0};
				RTP_FIXED_HEADER rtp_header;
				int rtp_header_size=sizeof(RTP_FIXED_HEADER);
				//RTP Header
				memcpy((void *)&rtp_header,recvData,rtp_header_size);
  
				//RFC3551
				char payload=rtp_header.payload;
				switch(payload){
				case 0:
				case 1:
				case 2:
				case 3:
				case 4:
				case 5:
				case 6:
				case 7:
				case 8:
				case 9:
				case 10:
				case 11:
				case 12:
				case 13:
				case 14:
				case 15:
				case 16:
				case 17:
				case 18: sprintf(payload_str,"Audio");break;
				case 31: sprintf(payload_str,"H.261");break;
				case 32: sprintf(payload_str,"MPV");break;
				case 33: sprintf(payload_str,"MP2T");break;
				case 34: sprintf(payload_str,"H.263");break;
				case 96: sprintf(payload_str,"H.264");break;
				default:sprintf(payload_str,"other");break;
				}
  
				unsigned int timestamp=ntohl(rtp_header.timestamp);
				unsigned int seq_no=ntohs(rtp_header.seq_no);
  
				fprintf(myout,"[RTP Pkt] %5d| %5s| %10u| %5d| %5d|\n",cnt,payload_str,timestamp,seq_no,pktsize);
  
				//RTP Data
				char *rtp_data=recvData+rtp_header_size;
				int rtp_data_size=pktsize-rtp_header_size;
				fwrite(rtp_data,rtp_data_size,1,fp1);
  
				//Parse MPEGTS
				if(parse_mpegts!=0&&payload==33){
					MPEGTS_FIXED_HEADER mpegts_header;
					for(int i=0;i<rtp_data_size;i=i+188){
						if(rtp_data[i]!=0x47)
							break;
						//MPEGTS Header
						//memcpy((void *)&mpegts_header,rtp_data+i,sizeof(MPEGTS_FIXED_HEADER));
						fprintf(myout,"   [MPEGTS Pkt]\n");
					}
				}
  
			}else{
				fprintf(myout,"[UDP Pkt] %5d| %5d|\n",cnt,pktsize);
				fwrite(recvData,pktsize,1,fp1);
			}
  
			cnt++;
		}
	}
	closesocket(serSocket); 
	WSACleanup();
	fclose(fp1);
  
	return 0;
}

上文中的函数调用方法如下所示。

simplest_udp_parser(8880);

结果

本程序输入为本机的一个端口号,输出为 UDP/RTP/MPEG-TS 的解析结果。程序开始运行后,可以使用推流软件向本机的 udp://127.0.0.1:8880 地址进行推流。例如可以使用 VLC Media Player 的 “打开媒体” 对话框中的 “串流” 功能(位于 “播放” 按钮旁边的小三角按钮的菜单中)。在该功能的对话框中添加一个 “RTP / MPEG Transport Stream” 的新目标。

也可以使用 FFmpeg 对本机的 8880 端口进行推流。下面的命令可以推流 UDP 封装的 MPEG-TS。

ffmpeg -re -i sintel.ts -f mpegts udp://127.0.0.1:8880

下面的命令可以推流首先经过 RTP 封装,然后经过 UDP 封装的 MPEG-TS。

ffmpeg -re -i sintel.ts -f rtp_mpegts udp://127.0.0.1:8880

推流之后,本文的程序会通过 Socket 接收到 UDP 包并且解析其中的数据。解析的结果如下图所示。

下载

Simplest mediadata test
项目主页

SourceForge:https://sourceforge.net/projects/simplest-mediadata-test/

Github:https://github.com/leixiaohua1020/simplest_mediadata_test

开源中国:http://git.oschina.net/leixiaohua1020/simplest_mediadata_test
CSDN 下载地址:http://download.csdn.net/detail/leixiaohua1020/9422409
本项目包含如下几种视音频数据解析示例:
 (1) 像素数据处理程序。包含 RGB 和 YUV 像素格式处理的函数。
 (2) 音频采样数据处理程序。包含 PCM 音频采样格式处理的函数。
 (3)H.264 码流分析程序。可以分离并解析 NALU。
 (4)AAC 码流分析程序。可以分离并解析 ADTS 帧。
 (5)FLV 封装格式分析程序。可以将 FLV 中的 MP3 音频码流分离出来。
 (6)UDP-RTP 协议分析程序。可以将分析 UDP/RTP/MPEG-TS 数据包。
雷霄骅 (Lei Xiaohua)
leixiaohua1020@126.com
http://blog.csdn.net/leixiaohua1020